package org.NooLab.tikaserver.tcp.box;

import java.io.*;
import java.net.*;
import java.util.Observable;
import java.util.Observer;
import java.util.Vector;

  
import org.NooLab.tikaserver.tcp.box.infra.DataReceiverIntf;
import org.NooLab.tikaserver.tcp.box.infra.ServerMsgHandlerIntf;
import org.NooLab.utilities.logging.LogControl;
import org.NooLab.utilities.logging.PrintLog;
import org.NooLab.utilities.objects.StringedObjects;



/**
 * 
 * acts as a listener for incoming messages 
 *  
 * 
 *
 */
public class TcpServer 
						extends 
									Observable 
						implements 
									Runnable, DataReceiverIntf{
	
	TcpMessageHandlerFactory msgFactory ;
	
	ServerSocket providerSocket;
	Socket connection = null;
	 
	int serverPort = -2, listenPort = -1;
	
	long cid=0;
	
	// the timeout should always be >0 !!!
	int clientConnexTimeOut = 3000 ;
	
	Thread connexThrd;
	ClientHandlerThread clientThread ;

	boolean serverStopped=false;
	Thread servingThrd;
	
	ServerMsgHandlerIntf serverMsg ;
	TcpBox parentTcpBox;
	
	Observer currentObserver;
	
	int socketState = -1 ;
	
	Vector<Integer> closedPorts = new Vector<Integer> ();
	
	PrintLog out = new PrintLog(2,true);
	StringedObjects sob = new StringedObjects();
	
	// ========================================================================
	// the same role as and like: UdpDataReceiver
	public TcpServer( TcpBox parentTcpbox,
					  int listenport, int ctrlport, 
					  Observer observer, ServerMsgHandlerIntf servermsg, 
					  String description) {
		
		listenPort = listenport;
		serverPort = listenport;
		
		currentObserver = observer;
		addObserver(observer) ;
		
		serverMsg = servermsg;
		parentTcpBox = parentTcpbox;
		
		socketState = openSocket();
		
						out.print(4, "TcpServer() created, listening to port "+listenport+
									 ", socketState="+socketState+" ");
	}
	
	public TcpServer( ServerMsgHandlerIntf servermsg ) {
		
		serverMsg = servermsg;
		
		socketState = openSocket();
		
		
	}
	// ========================================================================
	
	public int activate() {
		int result = 0;
		if (socketState==0){
			servingThrd = new Thread(this,"TcpServer");
			servingThrd.start() ;
			
		}else{
			System.err.println("Server listening to port "+listenPort+" can not start. \nProcess has been stopped.\n\n");
			servingThrd = null;
			socketState = -3;
			providerSocket = null;
			result = -3;
		}
		return result ;
	}

	protected void returnObject( Object obj ){
		
		// here we call the callback !
		out.print(5, "   --> tcp (port "+listenPort+"): returning object data through notifying Observer ("+currentObserver.toString()+") of TcpServer ... --> ");
		if (listenPort==7209){
			String str = (String)obj;
			out.print(6, "       message, very raw : \n\r-"+str);
		}
			
		// we decrypt the encoded stuff:  "<suid>#SUID#</suid>::#DATA#"
		String primaryString = (String) sob.decode( (String)obj );
		
		String suid = "";
		suid = _extractSuid(primaryString) ;
		String objString = _extractData(primaryString) ;
		
		String[] objData = new String[]{suid,objString}; 
		setChanged();
		this.notifyObservers( objData ) ;
	}

	private String _extractSuid(String str) {
		String suid="";
		
		int p1,p2,p3;
		
		p1 = str.indexOf("<suid");
		p2 = str.indexOf("</suid",p1+1);
		p3 = str.indexOf("::",p2+1);
		
		if ((p1>=0) && (p2>p1) && (p3>p2)){
			suid = str.substring( 6,p2) ;
		}
		
		return suid;
	}

	private String _extractData(String str) {
		String data="";
		
		int p1,p2,p3;
		
		p1 = str.indexOf("<suid");
		p2 = str.indexOf("</suid",p1+1);
		p3 = str.indexOf("::",p2+1);
		
		if ((p1>=0) && (p2>p1) && (p3>p2)){
			data = str.substring( p3+2, str.length() ) ;
		}
		
		return data;
	}

	// is threaded, but runs only once
	private void serverProcess(){
		try {
			
            if (providerSocket==null){
            	return;
            }
            // actually, address:port
            if ( closedPorts.contains( providerSocket.getLocalPort() )){
            	return;
            }
			// 2. Wait for connection
										out.print(5,"::: S: Waiting for connection at port "+serverPort);

										
			connection = providerSocket.accept();
			
			connection.setTcpNoDelay(true);
			connection.setReuseAddress(true);
										out.print(5,"::: S: Connection received from " + connection.getInetAddress().getHostName());
			
			cid++;
			// a new runnable object
			clientThread = new ClientHandlerThread( connection, cid , serverMsg, out);
			// creating a thread for it
			//connexThrd = 
			(new Thread(clientThread, "TCP - serverProcess() :: ClientHandlerThread")).start() ;
										out.print(5,"::: S: starting thread for handling client "+cid);
			// starting the thread							
			// connexThrd.start() ;
			
			
		} catch (Exception ioException) {
			try {
				// servingThrd.join() ;
			} catch (Exception e) { // InterruptedException 
			}
			
			
		} finally{
			if (providerSocket!=null){
				try {
					// providerSocket.close();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
		 
	}
	
	@SuppressWarnings("static-access")
	public void run() {
		
		try{

			while (serverStopped==false){
				
				serverProcess();
				Thread.currentThread().sleep(0, 10);
			}
			
		}catch(Exception e){
			
		}
		
												out.print(3,"\n>>>> server process (port "+listenPort+") has been stopped. <<<<");
		if ( parentTcpBox.transportInformer != null){
			parentTcpBox.transportInformer.setServerState(listenPort, -3);
		}
		
												out.print(3, "TcpServer, serverProcess() listening to port "+listenPort+
															 " has been stopped!");
	}
	
 
	
	public int openSocket() {
		
		int socketState=-1;
		
		// 1. creating a server socket
		try {
                                         		out.print(5,"creating a tcp server socket listening to port "+serverPort);
			providerSocket = new ServerSocket( serverPort, 5);
			providerSocket.setReuseAddress(true);
			providerSocket.setSoTimeout(clientConnexTimeOut) ;
			
			socketState = 0;
			
		} catch (Exception e) {
			socketState=-7;
			if (LogControl.Level >=3){
				System.err.println("Attempt to open socket: Connection denied for port "+serverPort+", "+e.getMessage() );
			}
		}
		return socketState ;
	}

	public void setObserver( Observer observer ){
		
		this.deleteObservers() ;
		currentObserver = observer;
		addObserver(observer) ;
	}
	
	 

	public int getListenPort() {
		
		return listenPort;
	}

	@Override
	public void closePort() {
		 
		try{
			
			if (clientThread!=null){
				clientThread.enforceConnectionShutDown=true;
			}
			
			if (providerSocket!=null){
				closedPorts.add( providerSocket.getLocalPort()) ;
			}else{
				
			}
			serverStopped  =true;
			
			listenPort = -1;
			
			// if (connexThrd!=null){
			if (clientThread!=null){
				//connexThrd.stopClientHandler() ;
				clientThread.stopClientHandler() ;
			}
			
			if (providerSocket!=null){
												out.print(3, "TcpServer, closing providerSocket (port "+providerSocket.getLocalPort()+").");
				if (providerSocket.isClosed()==false){
					providerSocket.close();
				}
				
				providerSocket = null;
			}else{
												out.print(3, "TcpServer, providerSocket (unknown) already closed.");
			}
			if (connection!=null){
				connection.close();
			}
			
			
			
		}catch(Exception e){
			e.printStackTrace();     
		}
		deleteObservers() ;

		
	}

	@Override
	public void deleteObservers() {
		 
		
	}

 



	class ClientHandlerThread implements Runnable {

		ServerMsgHandlerIntf serverMsg;

		long clientID,x=0;
		boolean enforceConnectionShutDown;

		Socket connection = null;
		ObjectOutputStream objOutStream;
		ObjectInputStream objInStream;

		PrintLog out;

		public ClientHandlerThread( Socket clientsocket, long clientid,
									ServerMsgHandlerIntf servermsg, PrintLog outprn) {

			connection = clientsocket;
			clientID = clientid;

			serverMsg = servermsg;

			out = outprn;
		}

		private void handlingClientConnection() {
			
			String rawmessage, responseStr = "";
			String message = "";
			int action = 1;

			try {

				// 3. get Input and Output streams
				objOutStream = new ObjectOutputStream( connection.getOutputStream() );
				
				objOutStream.flush();

				objInStream = new ObjectInputStream(connection.getInputStream());
				// we need
				sendMessage("<confirmation><client id=\""+clientID+"\"> </confirmation>\n"+
							"<!-- Connection to client " + clientID + " successfully established. -->");

				// 4. The two parts communicate via the input and output streams
				do {
					try {
												out.print(5, "s (c:" + clientID + ") handling... ");

						serverMsg.takeObjectStream(objInStream);

						/*
						// a ServerMsgHandler creates a response
						responseStr = serverMsg.getResponseString();

						action = serverMsg.getActionCode();

						rawmessage = serverMsg.getRawMessageAsString();
						*/
		// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>				
						rawmessage = (String) objInStream.readObject();

						message = rawmessage;
						returnObject(rawmessage);
						action = 0;
												out.print(5, "s (c:" + clientID + ")> " + message);
												out.print(5, "s -> c (" + clientID + ")> " + responseStr);

						if ((action < 0) || (rawmessage.contentEquals("bye"))) {// (message.equals("bye")){
							sendMessage("server says bye, bye (reason: code " + action + ") to client " + clientID + " ! ");
							sendMessage("<connection id=\""+clientID+"\"><close/> </connection>" );
						} else {

							// sendMessage("<xml>\n" + responseStr);
						}
						if (rawmessage.contentEquals("closed")) {
							// close this client thread
							enforceConnectionShutDown = true;
							// this will not close the server itself, just this
							// connection
						}

					} catch(EOFException efx){
						
					} catch(SocketException sex){
						
					} catch (Exception e) {
						e.printStackTrace();
					}

				} while ((action > 0) && (enforceConnectionShutDown == false)); // (!message.equals("bye"));

				out.print(5, "TcpServer, listening on port "+listenPort+"): Loop for message handling (cid:"+cid+", "+
							 "port:"+connection.getPort()+") has been left.");
				if (listenPort==7209){
					x++;
				}

			} catch (IOException ioException) {
				ioException.printStackTrace();
			} catch (Exception e) {
				e.printStackTrace();
			} finally {
				// 4: Closing connection
				try {

					if (connection.isClosed() == false) {
						out.print(5, "S: closing connection at port " + connection.getPort());

						if (objInStream != null) {
							objInStream.close();
						}

						if (objOutStream != null) {
							objOutStream.close();
						}

					}

				} catch (SocketException sex) {
					System.err.println("S: SocketException (1): " + sex.getMessage());
				} catch (IOException ioException) {
					ioException.printStackTrace();
				}
			}

		}

		@SuppressWarnings("static-access")
		private void delay( int millis ){
		
			try {
				Thread.currentThread().sleep(millis);
			} catch (InterruptedException e) {
			}
		}
		public void stopClientHandler(){
		
			enforceConnectionShutDown = true;
			delay(50);
			 
		}
		
		public void run() {

			handlingClientConnection();
		}

		void sendMessage(String msg) throws Exception {

			try {
				if (objOutStream != null) {
					objOutStream.writeObject(msg);
					objOutStream.flush();
				} else {
					throw (new Exception( "TcpServer: sendMessage() not possible, objOutStream = null"));
				}

			} catch (SocketException sex) {
				System.err.println("TcpServer: SocketException (2): " + sex.getMessage());
				enforceConnectionShutDown = true;
			} catch (IOException iex) {
				System.err.println("TcpServer: IOException (2): " + iex.getMessage());
				enforceConnectionShutDown = true;
				iex.printStackTrace();
			}
		}
	} // inner class

}